/*
 *  Created at 2011-12-06 21:18 GMT-03:00
 *  Author: Victor Williams Stafusa da Silva
 *  JSmartUnit
 */
package org.jsmartunit.invocations;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;

import org.jsmartunit.model.ClassStructure;
import org.jsmartunit.model.InstanceFactory;
import org.jsmartunit.model.TestRunner;
import org.jsmartunit.runners.MethodInvokerFactory;
import org.jsmartunit.statement.ExpectsException;
import org.jsmartunit.statement.NotifyingRunner;
import org.jsmartunit.statement.RunBeforesAfters;
import org.jsmartunit.statement.TimeoutRunner;
import org.jsmartunit.util.CollectionUtils;

import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;

/**
 * @author Victor Williams Stafusa da Silva
 */
public class MethodInvocation implements Invocation {

    private final RunNotifier notifier;
    private final InstanceFactory<?> instanceFactory;
    private final Method method;
    private final Object[] args;
    private final String name;

    public MethodInvocation(String name, RunNotifier notifier, InstanceFactory<?> instanceFactory, Method method, Object[] args) {
        if (name == null) throw new IllegalArgumentException();
        if (notifier == null) throw new IllegalArgumentException();
        if (instanceFactory == null) throw new IllegalArgumentException();
        if (method == null) throw new IllegalArgumentException();
        if (args == null) throw new IllegalArgumentException();
        if (!method.getDeclaringClass().isAssignableFrom(instanceFactory.getTargetClass())) throw new IllegalArgumentException();
        if (Modifier.isStatic(method.getModifiers())) throw new IllegalArgumentException();

        Class<?>[] formalParams = method.getParameterTypes();
        CollectionUtils.assertMatches(formalParams, args);

        this.name = name;
        this.notifier = notifier;
        this.instanceFactory = instanceFactory;
        this.method = method;
        this.args = args.clone();
    }

    public Object invokeExplosively() throws Throwable {
        return ClassStructure.invokeExplosively(instanceFactory, method, args);
    }

    public TestRunner createRunner() throws InitializationError {
        TestRunner methodRunner = new TestRunner(name, method.getAnnotations());

        Statement rec = new Invoker(methodRunner);
        rec = ExpectsException.withExpectedException(rec, method);
        rec = TimeoutRunner.withTimeout(rec, method);
        rec = RunBeforesAfters.aroundMethod(rec, instanceFactory, method);
        rec = NotifyingRunner.notifierFor(methodRunner, notifier, rec);

        methodRunner.setStatement(rec);

        return methodRunner;
    }

    private class Invoker extends Statement {

        private final TestRunner methodRunner;

        public Invoker(TestRunner methodRunner) {
            this.methodRunner = methodRunner;
        }

        @Override
        public void evaluate() throws Throwable {
            Object answer = invokeExplosively();
            if (answer == null) return;

            // The new scope is now the returned object. I.E. The @Test-annotated
            // methods will be called in that object.
            List<TestRunner> runners = new MethodInvokerFactory(name, notifier).runnersFor(answer);
            methodRunner.addChildren(runners);
            methodRunner.evaluateChildren();
        }
    }
}
